/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.runtime.module.jaas.loginmodule;

import org.mule.runtime.module.jaas.MuleJaasPrincipal;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

/**
 * This is the Default Login Module for the Mule Jaas Authentication. It extends Jaas' own LoginModule interface.
 */
public class DefaultLoginModule implements LoginModule {

  // Callback Handlers
  private CallbackHandler callbackHandler;

  // the authentication status
  private boolean succeeded = false;
  private boolean commitSucceeded = false;

  // username and password
  private String username;
  private String password;
  private String credentials;
  private List<String> credentialList;
  private Subject subject;

  /**
   * Abort if authentication fails
   *
   * @return boolean
   * @throws LoginException
   */
  public final boolean abort() throws LoginException {
    if (!succeeded) {
      return false;
    } else if (succeeded && !commitSucceeded) {
      // login succeeded but overall authentication failed
      succeeded = false;
      username = null;
      if (password != null) {
        password = null;
      }
    } else {
      // overall authentication succeeded and commit succeeded,
      // but someone else's commit failed
      logout();
    }
    return true;
  }

  /**
   * Commit if authentication succeeds, otherwise return false
   *
   * @return boolean
   * @throws LoginException
   */
  public final boolean commit() throws LoginException {
    if (!succeeded) {
      return false;
    } else {
      // set the principal just authenticated in the subject
      if (subject == null) {
        return false;
      }
      MuleJaasPrincipal principal = new MuleJaasPrincipal(username);
      Set entities = subject.getPrincipals();
      if (!entities.contains(principal)) {
        entities.add(principal);
      }

      // in any case, clean out state
      username = null;
      password = null;
      commitSucceeded = true;
      return true;
    }
  }

  /**
   * Initialises the callbackHandler, the credentials and the credentials list
   *
   * @param subject
   * @param callbackHandler
   * @param sharedState
   * @param options
   */
  public final void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
    this.subject = subject;
    this.callbackHandler = callbackHandler;

    this.credentials = (String) options.get("credentials");
    this.credentialList = getCredentialList(this.credentials);
  }

  /**
   * This method attempts to login the user by checking his credentials against those of the authorised users.
   *
   * @throws LoginException This is thrown either when there is no callback Handler or else when the user fails to be
   *         authenticated
   */
  public final boolean login() throws LoginException {
    if (callbackHandler == null) {
      throw new LoginException("Error: no CallbackHandler available " + "to garner authentication information from the user");
    }

    if (callbackHandler == null) {
      throw new LoginException("no handler");
    }

    NameCallback nameCb = new NameCallback("user: ");
    PasswordCallback passCb = new PasswordCallback("password: ", true);

    // Create the callbacks to send to the Callback Handler
    Callback[] callbacks = new Callback[] {nameCb, passCb};

    // Call the handler to get the username and password of the user.
    try {
      callbackHandler.handle(callbacks);
    } catch (IOException e) {
      throw new LoginException(e.toString());
    } catch (UnsupportedCallbackException e) {
      throw new LoginException("Error: " + e.getCallback().toString() + " not available to garner authentication information "
          + "from the user");
    }

    username = nameCb.getName();
    password = new String(passCb.getPassword());

    boolean usernameCorrect = false;
    boolean passwordCorrect = false;
    succeeded = false;

    // check the username and password against the list of authorised users
    for (int i = 0; i < credentialList.size(); i = i + 2) {
      if (username.equals(credentialList.get(i))) {
        usernameCorrect = true;
      } else {
        usernameCorrect = false;
      }

      if (password.equals(credentialList.get(i + 1))) {
        passwordCorrect = true;
      } else {
        passwordCorrect = false;
      }

      // only if both the username and password are correct will the user be
      // authenticated
      if ((usernameCorrect) & (passwordCorrect)) {
        succeeded = true;
      }
    }

    if (succeeded) {
      return true;
    } else {
      succeeded = false;
      username = null;
      password = null;
      if (!usernameCorrect) {
        throw new FailedLoginException("User Name Incorrect");
      } else {
        throw new FailedLoginException("Password Incorrect");
      }
    }
  }

  /**
   * Returns true when authentication succeeds or false when it fails
   *
   * @return succeeded
   */
  public final boolean logout() {
    return succeeded;
  }

  /**
   * This method parses the credentials string and populates the credentials list against which the username and password
   * submitted with the request will be checked
   *
   * @param creds
   * @return outputList
   */
  public final List<String> getCredentialList(String creds) {
    boolean semicolonIsFound = false;
    boolean dividerIsFound = false;
    char[] credentialArray = creds.toCharArray();
    String user = "";
    String passwd = "";
    List<String> outputList = new Vector<String>();

    for (int i = 0; i < creds.length(); i++) {
      if ((credentialArray[i] != ':') && (!dividerIsFound)) {
        user = user + credentialArray[i];
      } else if ((credentialArray[i] == ':') && (!dividerIsFound)) {
        dividerIsFound = true;
      } else if ((credentialArray[i] != ';') && (!semicolonIsFound) && (dividerIsFound)) {
        passwd = passwd + credentialArray[i];
      } else if ((credentialArray[i] != ';') && (!semicolonIsFound) && (dividerIsFound)) {
        passwd = passwd + credentialArray[i];
      } else if ((credentialArray[i] == ';') && (!semicolonIsFound) && (dividerIsFound)) {
        outputList.add(user);
        outputList.add(passwd);
        semicolonIsFound = false;
        dividerIsFound = false;
        user = "";
        passwd = "";
      }
    }
    return outputList;
  }
}
